/// <reference path="../typings/jquery/jquery.d.ts" />
/// <reference path="../typings/knockout/knockout.d.ts" />
/// <reference path="../disco/disco.ts" />

module disco.model {

    declare var OData;

    export interface DataServices {
        schema: any[];
    }

    export class Metadata {
        public version: string;
        public dataServices: DataServices;
    }

    export class DataContext {
        public metadata: Metadata;
        public serviceUri: string;
        public entityTypes: any = {};
        public entitySets: any = {};

        public entityType(name: string, value: EntityType = undefined): EntityType {
            if (value)
                return this.entityTypes[name] = value;
            else
                return this.entityTypes[name];
        }

        public entitySet(typeName: string, value: EntitySet = undefined): EntitySet {
            if (value)
                return this.entitySets[typeName] = value;
            else
                return this.entitySets[typeName];
        }
    }

    export class EntityType {
        public name: string;
        public entitySet: EntitySet = null;
        public properties: EntityProperty[] = [];
        public navigationProperties: NavigationProperty[] = [];
        public baseType: EntityType = null;
        public metadata: any = null;

        public toString(): string {
            return '<' + this.name + '>';
        }

        public useToStringForStringify = true;
    }

    export class EntityProperty {
        public name: string;
        public isInherited: boolean = false;
        public isKey: boolean = false;
        public isNullable: boolean = true;
        public isMany: boolean = false;
        public typeString: string;
        public type: EntityType = null;
        public isNavigation: boolean = false;

        public toString(): string {
            return '<' + this.name + ': ' + this.typeString + '>';
        }

        public useToStringForStringify = true;
    }

    export class NavigationProperty {
        public name: string;
        public inherited: boolean = false;
        public from: AssociationEnd;
        public to: AssociationEnd;
    }

    export class AssociationEnd {
        public role: string;
        public type: string;
        public multiplicity: string;
    }

    export class EntityCollection {
        public rows: KnockoutObservableArray<Entity> = ko.observableArray<Entity>([]);
    }

    export class EntitySet extends EntityCollection {
        public name: string;
        public type: EntityType;
    }

    export class Entity {
        public type: model.EntityType;
        public properties: any = {};

        public property(name: string, value: PropertyValue = undefined): PropertyValue {
            if (value)
                return this.properties[name] = value;
            else
                return this.properties[name];
        }

        public isEntity = true;
    }

    export class PropertyValue {
        public property: model.EntityProperty;
        //public loaded: boolean = false;
        public url: string;
        public value: any;
    }

    export class OntologyModel {
        private dataContext: KnockoutObservable<DataContext> = ko.observable<DataContext>();

        public getDataContext(): KnockoutObservable<DataContext> {
            return this.dataContext;
        }

        public load(serviceUri: string, success: (ontology: OntologyModel) => void = o => { }): void {
            OData.read(
                serviceUri + "$metadata",
                (data, response) => {
                    //success
                    var context = new DataContext();
                    context.serviceUri = serviceUri;
                    context.metadata = data;

                    var contextMgr = new helper.DataContextManager(context);
                    contextMgr.generateEntityModel();

                    this.dataContext(context);
                    success(this);
                },
                (error) => {
                    //error
                    //TODO: error handling
                },
                OData.metadataHandler);
        }
    }

    export class EntityTypeManager {
        private entityType: EntityType;

        constructor(entityType: EntityType = null) {
            this.entityType = entityType;
        }

        public getEntityType(): EntityType {
            return this.entityType;
        }
        public setEntityType(value: EntityType = new EntityType()): EntityType {
            return this.entityType = value;
        }

        public each(types: EntityType[], callback: (typeMgr: EntityTypeManager, index: number, types: EntityType[]) => void) {
            types.forEach((type: EntityType, index: number) => {
                this.setEntityType(type);
                callback.call(this, this, index, types);
            }, this);
        }

        public generateSimpleProperties(metaEntityType: any, associations: any[]) {
            var type: EntityType = this.getEntityType();
            type.metadata = metaEntityType;
            type.name = metaEntityType.name;

            if (type.metadata.property) {
                var keyPropertyName: string = type.metadata.key ? type.metadata.key.propertyRef.name : null;

                var propertyMgr: EntityPropertyManager = new EntityPropertyManager();

                type.metadata.property.forEach((metaProperty) => {
                    propertyMgr.generateEntityProperty(metaProperty, keyPropertyName);
                    type.properties.push(propertyMgr.getEntityProperty());
                });
            }
            if (type.metadata.navigationProperty) {
                var navigationPropertyMgr: NavigationPropertyManager = new NavigationPropertyManager();

                type.metadata.navigationProperty.forEach((metaProperty) => {
                    navigationPropertyMgr.generateNavigationProperty(metaProperty, associations);
                    type.navigationProperties.push(navigationPropertyMgr.getNavigationProperty());
                });
            }
        }
        public applyBaseType(context: DataContext) {
            var type = this.getEntityType();
            if (type && type.metadata.baseType && !type.baseType) {
                var baseTypeName = Namespace.remove(type.metadata.baseType);
                type.baseType = context.entityType(baseTypeName);
                
                new EntityTypeManager(type.baseType).applyBaseType(context);

                var entityPropertyMgr: EntityPropertyManager = new EntityPropertyManager();
                var inheritedEntityProperties = entityPropertyMgr.copyProperties(type.baseType.properties);
                inheritedEntityProperties.forEach((property: EntityProperty) => { property.isInherited = true });
                type.properties = inheritedEntityProperties.concat(type.properties);

                var navigationPropertyMgr: NavigationPropertyManager = new NavigationPropertyManager();
                var inheritedNavigationProperties = navigationPropertyMgr.copyProperties(type.baseType.navigationProperties);
                inheritedNavigationProperties.forEach((property: NavigationProperty) => { property.inherited = true });
                type.navigationProperties = inheritedNavigationProperties.concat(type.navigationProperties);
            }
        }
        public addNavigationToEntityProperties(context: DataContext) {
            var entityPropertyMgr: EntityPropertyManager = new EntityPropertyManager();
            var type: EntityType = this.getEntityType()

            var convertedProperties: EntityProperty[] = type.navigationProperties.map(
                (navigationProperty: NavigationProperty) => entityPropertyMgr.fromNavigationProperty(navigationProperty, context));
            type.properties = type.properties.concat(convertedProperties);
        }
    }

    export class EntityPropertyManager {
        private entityProperty: EntityProperty;

        constructor(entityProperty: EntityProperty = null) {
            this.entityProperty = entityProperty;
        }

        public getEntityProperty(): EntityProperty {
            return this.entityProperty;
        }

        public setEntityProperty(entityProperty: EntityProperty = new EntityProperty()): EntityProperty {
            return this.entityProperty = entityProperty;
        }

        public generateEntityProperty(metaProperty: any, keyPropertyName: string) {
            var property: EntityProperty = this.setEntityProperty();
            property.name = metaProperty.name;
            property.typeString = metaProperty.type;
            property.isNullable = metaProperty.nullable || true;
            property.isKey = metaProperty.name == keyPropertyName;
        }

        public fromNavigationProperty(navigationProperty: NavigationProperty, context: DataContext): EntityProperty {
            var entityProperty: EntityProperty = this.setEntityProperty();
            var associationEnd: AssociationEnd = navigationProperty.to;

            entityProperty.isNavigation = true;
            entityProperty.name = associationEnd.role;
            entityProperty.isMany = associationEnd.multiplicity == '*';
            entityProperty.isNullable = associationEnd.multiplicity != '1';
            entityProperty.typeString = Namespace.remove(associationEnd.type);
            entityProperty.type = context.entityType(entityProperty.typeString);

            return entityProperty;
        }

        public copyProperty(property: EntityProperty): EntityProperty {
            var p: EntityProperty = property;
            var ret: EntityProperty = { 
                isInherited: p.isInherited, isKey: p.isKey, isMany: p.isMany, isNavigation: p.isNavigation,
                isNullable: p.isNullable, name: p.name, type: p.type, typeString: p.typeString, toString: EntityProperty.prototype.toString, useToStringForStringify: p.useToStringForStringify
            };
            return this.setEntityProperty(ret);
        }
        public copyProperties(properties: EntityProperty[]): EntityProperty[]{
            var self: EntityPropertyManager = this;
            return properties.map((property: EntityProperty) => self.copyProperty(property));
        }
    }

    export class NavigationPropertyManager {
        private navigationProperty: NavigationProperty;

        constructor(navigationProperty: NavigationProperty = null) {
            this.navigationProperty = navigationProperty;
        }

        public getNavigationProperty(): NavigationProperty {
            return this.navigationProperty;
        }

        public setNavigationProperty(property: NavigationProperty = new NavigationProperty()): NavigationProperty {
            return this.navigationProperty = property;
        }

        public generateNavigationProperty(metaProperty: any, metaAssociations: any[]) {
            var property = this.setNavigationProperty();
            property.name = metaProperty.name;

            var metaAssociationName = Namespace.remove(metaProperty.relationship);
            var metaAssociation = Collections.single(metaAssociations, (metaAssociation) => metaAssociationName == metaAssociation.name);
            property.from = Collections.single(metaAssociation.end, (associationEnd: AssociationEnd) => metaProperty.fromRole == associationEnd.role);
            property.to = Collections.single(metaAssociation.end, (associationEnd: AssociationEnd) => metaProperty.toRole == associationEnd.role);
        }

        public copyProperty(property: NavigationProperty): NavigationProperty {
            var p: NavigationProperty = property;
            var ret: NavigationProperty = {
                from: p.from, inherited: p.inherited, name: p.name, to: p.to
            };
            return this.setNavigationProperty(ret);
        }

        public copyProperties(properties: NavigationProperty[]): NavigationProperty[]{
            var self: NavigationPropertyManager = this;
            return properties.map((property: NavigationProperty) => self.copyProperty(property));
        }
    }

    export var stringifyReplacer = (key, value) => {

        var replacePropertyValue = val => {
            if (typeof val == 'object') {
                if (val.isEntity) {
                    return '<' + val.type.name + ' #' + val.property('Id').value + '>';
                }
                else if (Type.get(val) == '[object Array]') {
                    return (<Array>val).map(replacePropertyValue);
                }
                else
                    return val;
            }
            else
                return val;
        };

        if (value && value.useToStringForStringify) {
            return value.toString();
        }
        if (key == 'value')
            return replacePropertyValue(value);
        return value;
    };
}